''' <summary>
''' Class to read and write BMD files, allowing data to be loaded entirely into memory or kept on disk
''' This is not programmed to allow writing if InRam
''' </summary>
Public Class clsBMD

    'BMD file structure is configured as follows:
    '   <HEADER>            (78 bytes; see below for details)
    '   <VARIABLES>         (18 char for name followed by 12 char for units)
    '   <CONCS>             (singles arranged by Segment, Time, then Variable)
    '   <TIMES>             (doubles: number of days since seed year)
    '   <MIN/MAX VAR>       (single pairs of min/max values for each variable)
    '   <MIN/MAX SEG-VAR>   (single pairs of min/max values for each segment and variable)
    '   <SEGNAMES>          (15 char names for each segment)

    ''' <summary>
    ''' Constants with sizes of selected parts of BMD file
    ''' </summary>
    Private Const HeaderSize As Integer = 78
    Private Const SpaceSize As Integer = 29
    Private Const VarSize As Integer = VarNameSize + VarUnitSize
    Private Const VarNameSize As Integer = 18
    Private Const VarUnitSize As Integer = 12
    Private Const ConcSize As Integer = 4
    Private Const TimeSize As Integer = 8
    Private Const SegNameSize As Integer = 15

    'use different buffer sized depending on whether loading entire file into RAM or not
    Private Const BufferSize_Large As Integer = 102400
    Private Const BufferSize_Small As Integer = 128

    ''' <summary>Use this error text if error detected when allocating arrays</summary>
    Private Const MemoryError As String = "Insufficient memory available to process this BMD file in RAM; configure software to use disk-based option."

    Private Structure structBMDHeader
        <VBFixedString(3), System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst:=3)> Dim Signature As String
        <VBFixedString(1), System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst:=1)> Dim SourceType As String
        <VBFixedString(1), System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst:=1)> Dim Producer As String
        Dim Version As Single
        Dim OldSeedTime As UInteger 'this is for older BMD files that used a single unsigned long value relative to 1/1/1970 but may have problems with DST
        Dim SeedSecond As Integer   'the following are a new representation of the seed time that is based on the julian day and second
        Dim SeedJDay As Integer
        <VBFixedString(SpaceSize), System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst:=SpaceSize)> Dim Spaces As String
        Dim NumSegments As Integer
        Dim NumTimes As Integer
        Dim NumVars As Integer
        Dim StartTime As Double
        Dim EndTime As Double
    End Structure

    ''' <summary>
    ''' Locations of selected items in header (see locXXX variables for more locations)
    ''' </summary>
    Private Enum enumLocation
        locSeedTime = 9
        locNumSegments = 50
        locNumTimes = 54
        locNumVars = 58
        locStartTime = 62
        locEndTime = 70
        locVars = 78
    End Enum

    'following will be computed once the size of the dimensions are known
    'note: cannot write times until know total number of timesteps (which determines size of conc block)
    Private locConc As Long
    Private locMinMaxOverVars As Long
    Private locMinMaxOverVarSegs As Long
    Private locSegmentNames As Long
    Private locTimes As Long

    Private _BR As IO.BinaryReader
    Private _BW As IO.BinaryWriter
    Private _File As IO.FileStream
    Private _CreateFile As Boolean

    ''' <summary>
    ''' If true, concentrations are loaded into RAM for quick access (may cause out of memory errors); all other items are stored in RAM regardless of this setting to reduce disk access
    ''' </summary>
    Private _InRam As Boolean

    Private _FileName As String     'full path to BMD file

    Private _Header As structBMDHeader
    Private _VarNames() As String
    Private _VarUnits() As String

    ''' <summary>Array holding concentrations in memory (indices are time, segment, var); not used if _InRam is false</summary>
    Private _Conc(,,) As Single

    Private _Times() As Double 'always stored in RAM
    Private _MinOverVars() As Single
    Private _MaxOverVars() As Single

    ''' <summary>Array holding min values for each segment and variagle (indices are var, seg)</summary>
    Private _MinOverVarSegs(,) As Single

    ''' <summary>Array holding max values for each segment and variagle (indices are var, seg)</summary>
    Private _MaxOverVarSegs(,) As Single

    Private _SegmentNames() As String

    Private _SeedDate As Date

    Public Event RevertingToDiskBased()

    Public Sub New(ByVal FileName As String, ByVal CreateFile As Boolean, Optional ByVal SeedYear As Integer = 1970, Optional ByVal InRam As Boolean = True, Optional ByVal NumTimes As Integer = 0, Optional ByVal NumSegments As Integer = 0, Optional ByVal NumVars As Integer = 0)
        Initialize(FileName, CreateFile, SeedYear, InRam, NumTimes, NumSegments, NumVars)
    End Sub

    Public Sub New()
    End Sub

    ''' <summary>
    ''' Prepare new BMD file for writing
    ''' </summary>
    ''' <param name="Filename"></param>
    ''' <param name="CreateFile"></param>
    ''' <param name="SeedYear"></param>
    ''' <param name="InRam"></param>
    ''' <param name="NumTimes"></param>
    ''' <param name="NumSegments"></param>
    ''' <param name="NumVars"></param>
    ''' <remarks></remarks>
    Public Sub Initialize(ByVal Filename As String, ByVal CreateFile As Boolean, Optional ByVal SeedYear As Integer = 1970, Optional ByVal InRam As Boolean = True, Optional ByVal NumTimes As Integer = 0, Optional ByVal NumSegments As Integer = 0, Optional ByVal NumVars As Integer = 0)
        _FileName = Filename
        _CreateFile = CreateFile
        With _Header
            If CreateFile Then
                If IO.File.Exists(Filename) Then IO.File.Delete(Filename)
                .OldSeedTime = 0
                'If SeedYear < 0 Then SeedYear += 1 'not sure about this
                '.SeedJDay = ((SeedYear + 4800) * 1461 + 1) / 4 - 32083
                '.SeedJDay += 1 'had to add one day by inspection comparing MOVEM to Graph

                _SeedDate = New Date(SeedYear, 1, 1)
                .SeedJDay = GregorianToJulian(_SeedDate)
                .SeedSecond = 0

                .Spaces = "".PadRight(SpaceSize, " ")

                _InRam = False

                .NumTimes = NumTimes
                .NumSegments = NumSegments
                .NumVars = NumVars

                Try
                    If .NumVars > 0 Then
                        ReDim _VarNames(.NumVars - 1)
                        ReDim _VarUnits(.NumVars - 1)
                    Else 'will dynamically resize as needed when set property
                        ReDim _VarNames(5)
                        ReDim _VarUnits(5)
                    End If
                    If .NumSegments > 0 Then
                        ReDim _SegmentNames(.NumSegments - 1)
                    Else
                        ReDim _SegmentNames(5)
                    End If
                    If .NumTimes > 0 Then
                        ReDim _Times(.NumTimes - 1)
                    Else
                        ReDim _Times(500)
                    End If
                Catch ex As OutOfMemoryException
                    Throw New OutOfMemoryException(MemoryError, ex)
                    Exit Sub
                End Try

                ComputeLocs()

                _File = New IO.FileStream(_FileName, IO.FileMode.CreateNew, IO.FileAccess.Write, IO.FileShare.None, BufferSize_Large, IO.FileOptions.RandomAccess)
                _BW = New IO.BinaryWriter(_File, System.Text.Encoding.ASCII)
            Else
                _InRam = InRam
                If NumTimes + NumSegments + NumVars <> 0 Then
                    Throw New ApplicationException("clsBMDDisk.Initialize: do not specify dimensions unles CreateFile is true!")
                    Exit Sub
                End If
                If Not IO.File.Exists(Filename) Then Throw New System.IO.FileNotFoundException : Exit Sub
                _File = New IO.FileStream(_FileName, IO.FileMode.Open, IO.FileAccess.Read, IO.FileShare.Read, BufferSize_Large, IO.FileOptions.RandomAccess)
                _BR = New IO.BinaryReader(_File, System.Text.Encoding.ASCII)
                ReadHeader()
            End If
        End With
    End Sub

    ''' <summary>
    ''' Convert internal double time value to normal date
    ''' </summary>
    Private Function GetDate(ByVal TimeValue As Double) As Date
        Return RoundDateTime(_SeedDate.AddDays(TimeValue))
    End Function

    ''' <summary>
    ''' Convert date to internal double time value
    ''' </summary>
    Private Function GetDouble(ByVal DateTime As Date) As Double
        Return DateTime.Subtract(_SeedDate).TotalDays
    End Function

    ''' <summary>
    ''' Close BMD file (after rewriting header, times, min/max, and segment names if appropriate, and all concs (if _InRam))
    ''' </summary>
    Public Sub Close()
        If _CreateFile Then

            With _Header
                .Signature = "BMD"
                .SourceType = Chr(15)
                .Producer = Chr(9)
                .Version = 2.0
                .StartTime = _Times(0)
                .EndTime = _Times(_Times.Length - 1)

                _BW.Seek(0, IO.SeekOrigin.Begin)
                _BW.Write(.Signature.ToCharArray)
                _BW.Write(.SourceType.ToCharArray)
                _BW.Write(.Producer.ToCharArray)
                _BW.Write(.Version)
                _BW.Write(.OldSeedTime)
                _BW.Write(.SeedSecond)
                _BW.Write(.SeedJDay)
                _BW.Write(.Spaces.ToCharArray)
                _BW.Write(.NumSegments)
                _BW.Write(.NumTimes)
                _BW.Write(.NumVars)
                _BW.Write(.StartTime)
                _BW.Write(.EndTime)

                'write variable names and units

                For i As Integer = 0 To .NumVars - 1
                    _BW.Write((TestNull(_VarNames(i), "").Trim.PadRight(VarNameSize, Chr(0)) & TestNull(_VarUnits(i), "").Trim.PadRight(VarUnitSize, Chr(0))).ToCharArray)
                Next

                ComputeLocs()

                Dim byt() As Byte = Nothing

                'write concentrations if stored in ram

                If _InRam Then
                    If _Conc Is Nothing Then ReDim _Conc(.NumTimes - 1, .NumSegments - 1, .NumVars - 1) 'will just write all zeros
                    _File.Seek(locConc, IO.SeekOrigin.Begin)
                    Try
                        ReDim byt(_Header.NumTimes * _Header.NumSegments * _Header.NumVars * ConcSize - 1)
                        Buffer.BlockCopy(_Conc, 0, byt, 0, byt.Length)
                        _BW.Write(byt, 0, byt.Length)
                    Catch ex As OutOfMemoryException
                        Throw New OutOfMemoryException(MemoryError, ex)
                        Exit Sub
                    Finally
                        If byt IsNot Nothing Then Array.Clear(byt, 0, byt.Length)
                    End Try

                    RecalcMinMax()
                Else
                    _File.Seek(locTimes, IO.SeekOrigin.Begin)
                End If

                'write times

                Try
                    ReDim byt(_Header.NumTimes * TimeSize - 1)
                    Buffer.BlockCopy(_Times, 0, byt, 0, byt.Length)
                    _BW.Write(byt, 0, byt.Length)
                Catch ex As OutOfMemoryException
                    Throw New OutOfMemoryException(MemoryError, ex)
                    Exit Sub
                Finally
                    If byt IsNot Nothing Then Array.Clear(byt, 0, byt.Length)
                End Try

                'write min/max

                For i As Integer = 0 To .NumVars - 1
                    _BW.Write(_MinOverVars(i))
                    _BW.Write(_MaxOverVars(i))
                Next

                For i As Integer = 0 To .NumVars - 1
                    For j As Integer = 0 To .NumSegments - 1
                        _BW.Write(_MinOverVarSegs(i, j))
                        _BW.Write(_MaxOverVarSegs(i, j))
                    Next
                Next

                'write segment names

                For s As Integer = 0 To .NumSegments - 1
                    _BW.Write(NullString(_SegmentNames(s), SegNameSize))
                Next
            End With

        End If
        If _BR IsNot Nothing Then _BR.Close()
        If _BW IsNot Nothing Then _BW.Close()
        _File.Close()
    End Sub

    ''' <summary>
    ''' Convert VB string to null-terminated string suitable for C++ reading
    ''' </summary>
    ''' <param name="s">String to convert</param>
    ''' <param name="length">Fixed length</param>
    ''' <remarks>Must reserver last position for null character</remarks>
    Private Function NullString(ByVal s As String, ByVal length As Integer) As Char()
        Dim c() As Char = s.ToCharArray
        ReDim Preserve c(length - 1)
        c(length - 1) = Chr(0)
        Return c
    End Function

    ''' <summary>
    ''' Once all the dimensions are known, compute the location of various blocks of data
    ''' </summary>
    Private Sub ComputeLocs()
        locConc = enumLocation.locVars + CLng(_Header.NumVars) * VarSize
        locTimes = locConc + CLng(_Header.NumVars) * _Header.NumSegments * _Header.NumTimes * ConcSize
        locMinMaxOverVars = locTimes + CLng(_Header.NumTimes) * TimeSize
        locMinMaxOverVarSegs = locMinMaxOverVars + CLng(_Header.NumVars) * ConcSize * 2
        locSegmentNames = locMinMaxOverVarSegs + CLng(_Header.NumVars) * _Header.NumSegments * ConcSize * 2
    End Sub

    ''' <summary>
    ''' Read BMD header record, var names, segment names, and min/max (times are read on demand; concs are read from disk or read on demand depending on _InRam)
    ''' </summary>
    Private Sub ReadHeader()
        Try
            _File.Seek(0, IO.SeekOrigin.Begin)
            With _Header
                .Signature = _BR.ReadChars(3)
                .SourceType = _BR.ReadChars(1)
                .Producer = _BR.ReadChars(1)
                .Version = _BR.ReadSingle
                .OldSeedTime = _BR.ReadUInt32
                .SeedSecond = _BR.ReadInt32
                .SeedJDay = _BR.ReadInt32
                .Spaces = _BR.ReadChars(SpaceSize)
                .NumSegments = _BR.ReadInt32
                .NumTimes = _BR.ReadInt32
                .NumVars = _BR.ReadInt32
                .StartTime = _BR.ReadDouble
                .EndTime = _BR.ReadDouble

                If .SeedJDay <> 0 And .OldSeedTime = 0 Then

                    _SeedDate = JulianToGregorian(.SeedJDay).AddSeconds(.SeedSecond)

                    'I couldn't get this even close!

                    ''following taken from code provided by Alex Comer based on SDN package by Scott Lee for handling Julian dates
                    'Dim SDN_OFFSET As Integer = 32083
                    'Dim DAYS_PER_4_YEARS As Integer = 1461
                    'Dim DAYS_PER_5_MONTHS As Integer = 153

                    ''calculate year and day of year (btwn 1 and 366)

                    'Dim temp As Integer = (.SeedJDay + SDN_OFFSET) * 4 - 1
                    'Dim Year As Integer = temp / DAYS_PER_4_YEARS
                    'Dim DayOfYear As Integer = (temp Mod DAYS_PER_4_YEARS) / 4 + 1

                    ''calculate month and day of month

                    'temp = DayOfYear * 5 - 3
                    'Dim month As Integer = temp / DAYS_PER_5_MONTHS
                    'Dim day As Integer = (temp Mod DAYS_PER_5_MONTHS) / 5 + 1

                    ''convert to normal beginning of year
                    'If month < 10 Then month += 3 Else Year += 1 : month -= 9

                    ''adjust to bc/ad type numbering
                    'Year -= 4800
                    'If Year <= 0 Then Year -= 1 'not sure about this; C++ code has Year--
                    '_SeedDate = New Date(Year, month, day).AddSeconds(.SeedSecond) 'had to subtract one day by inspection comparing MOVEM to Graph

                ElseIf .OldSeedTime <> 0 Then
                    _SeedDate = New Date(1901, 1, 1).AddSeconds(.OldSeedTime)
                Else
                    _SeedDate = New Date(1970, 1, 1)
                End If

                'read variable names and units

                ReDim _VarNames(.NumVars - 1)
                ReDim _VarUnits(.NumVars - 1)
                For i As Integer = 0 To .NumVars - 1
                    _VarNames(i) = Replace(_BR.ReadChars(VarNameSize), Chr(0), "").Trim
                    _VarUnits(i) = Replace(_BR.ReadChars(VarUnitSize), Chr(0), "").Trim
                Next

                ComputeLocs()

                'skip past times and concs and read stuff at bottom (if _InRAM is true, Concs and Times are read on demand)

                _File.Seek(locMinMaxOverVars, IO.SeekOrigin.Begin)

                'read min/max

                ReDim _MinOverVars(.NumVars - 1), _MaxOverVars(.NumVars - 1)
                For i As Integer = 0 To .NumVars - 1
                    _MinOverVars(i) = _BR.ReadSingle
                    _MaxOverVars(i) = _BR.ReadSingle
                Next

                ReDim _MinOverVarSegs(.NumVars - 1, .NumSegments - 1), _MaxOverVarSegs(.NumVars - 1, .NumSegments - 1)
                For i As Integer = 0 To .NumVars - 1
                    For j As Integer = 0 To .NumSegments - 1
                        _MinOverVarSegs(i, j) = _BR.ReadSingle
                        _MaxOverVarSegs(i, j) = _BR.ReadSingle
                    Next
                Next

                'read segment names; not always included in bmd files
                ReDim _SegmentNames(.NumSegments - 1)
                Try
                    For i As Integer = 0 To .NumSegments - 1
                        _SegmentNames(i) = _BR.ReadChars(SegNameSize)
                        _SegmentNames(i) = _SegmentNames(i).Replace(Chr(0), "").Trim
                    Next
                Catch ex As Exception
                    For i As Integer = 0 To .NumSegments - 1
                        _SegmentNames(i) = String.Format("Segment {0:00}", i)
                    Next
                End Try

            End With
        Catch ex As Exception
            If _BR IsNot Nothing Then _BR.Close()
            If _File IsNot Nothing Then _File.Close()
            Throw New System.IO.InvalidDataException("Error occurred trying to read the header of the BMD file named: " & _FileName & "; the file may be corrupted.", ex)
        End Try
    End Sub

    ''' <summary>Note: constant was determined by trial and error to force match with MOVEM!</summary>
    Private Const DATESHIFT As Integer = 1721439

    ''' <summary>
    ''' Convert gregorian date to julian
    ''' </summary>
    Private Function GregorianToJulian(ByVal DateTime As Date) As Integer
        Dim dtStart As Date = New DateTime(1, 1, 1)
        Dim ts As New TimeSpan(DateTime.Ticks - dtStart.Ticks)
        Return ts.TotalDays + DATESHIFT
    End Function

    ''' <summary>
    ''' Convert julian date to gregorian
    ''' </summary>
    Private Function JulianToGregorian(ByVal Julian As Integer) As Date
        Dim totaldays As Integer = Julian - DATESHIFT
        Return New DateTime(1, 1, 1).AddDays(totaldays)
    End Function

    ''' <summary>
    ''' Always read all times into RAM, regardless of _InRam value
    ''' </summary>
    Private Sub ReadTimes()
        With _Header
            _File.Seek(locTimes, IO.SeekOrigin.Begin)
            Dim byt() As Byte = Nothing
            Try
                ReDim _Times(.NumTimes - 1)
                ReDim byt(.NumTimes * TimeSize - 1)
                _BR.Read(byt, 0, byt.Length)
                Buffer.BlockCopy(byt, 0, _Times, 0, byt.Length)
            Catch ex As OutOfMemoryException
                Throw New OutOfMemoryException(MemoryError, ex)
                Exit Sub
            Finally
                If byt IsNot Nothing Then Array.Clear(byt, 0, byt.Length)
            End Try
        End With
    End Sub

    ''' <summary>
    ''' Load all concentrations into memory if _InRam is true
    ''' </summary>
    ''' <returns>True if successful</returns>
    Private Function ReadConcs() As Boolean
        If _Conc IsNot Nothing Then Return True
        If Not RedimConcs() Then Return False
        With _Header
            _File.Seek(locConc, IO.SeekOrigin.Begin)
            Dim byt() As Byte = Nothing
            Try

                'following is convenient test for handling large BMD files
                'If _InRam Then
                '    ReDim _Conc(40, 250, 50000)
                'End If

                'use following procedure to really speed up the read operation
                ReDim byt(.NumVars * .NumSegments * .NumTimes * ConcSize - 1)
                _BR.Read(byt, 0, byt.Length)
                'note: data are stored in a different order, so can't read directly into array; rearrange using properties
                Buffer.BlockCopy(byt, 0, _Conc, 0, byt.Length)

            Catch ex As OutOfMemoryException
                _Conc = Nothing
                _InRam = False
                RaiseEvent RevertingToDiskBased()
                Return False
            Finally
                If byt IsNot Nothing Then Array.Clear(byt, 0, byt.Length)
            End Try
            Return True
        End With
    End Function

    ''' <summary>
    ''' If Conc array has not been allocated, do so
    ''' </summary>
    ''' <returns>True if successful</returns>
    Private Function RedimConcs() As Boolean
        If _Conc IsNot Nothing Or Not _InRam Then Return True 'already read
        With _Header
            Try
                'Dim OutOfMemory As Boolean
                'Do
                '    OutOfMemory = False
                '    NumPages += 1
                '    Try
                '        ReDim _Conc(.NumTimes / NumPages - 1, .NumSegments - 1, .NumVars - 1)
                '    Catch ex As Exception
                '        OutOfMemory = True
                '    End Try
                'Loop Until Not OutOfMemory

                ReDim _Conc(.NumTimes - 1, .NumSegments - 1, .NumVars - 1)
                Return True
            Catch ex As OutOfMemoryException
                'Throw New OutOfMemoryException(MemoryError, ex)
                'change _InRam flag and force to read from disk

                'reopen the filestream using a much smaller buffer to improve performance
                _BR.Close()
                _File.Close()
                _File = New IO.FileStream(_FileName, IO.FileMode.Open, IO.FileAccess.Read, IO.FileShare.Read, BufferSize_Small, IO.FileOptions.RandomAccess)
                _BR = New IO.BinaryReader(_File, System.Text.Encoding.ASCII)


                'todo: consider writing a temporary file with the data reorganized for optimal display of time series data
                'would be organized by Station then PCode then Times so could jump to Sta/PCode and read entire time series
                'at once. Problem is that it could take a long time to rearrange the data!

                RaiseEvent RevertingToDiskBased()
                _InRam = False
                Return False
            End Try
        End With
    End Function

    ''' <summary>
    ''' Reset min/max values for each variable
    ''' </summary>
    Private Sub ResetMinMax()
        'redimension min/max variables
        ReDim _MinOverVars(_Header.NumVars - 1)
        ReDim _MaxOverVars(_Header.NumVars - 1)
        For i As Integer = 0 To _Header.NumVars - 1
            _MinOverVars(i) = Single.MaxValue
            _MaxOverVars(i) = Single.MinValue
        Next
        ReDim _MinOverVarSegs(_Header.NumVars - 1, _Header.NumSegments - 1)
        ReDim _MaxOverVarSegs(_Header.NumVars - 1, _Header.NumSegments - 1)
        For i As Integer = 0 To _Header.NumVars - 1
            For j As Integer = 0 To _Header.NumSegments - 1
                _MinOverVarSegs(i, j) = Single.MaxValue
                _MaxOverVarSegs(i, j) = Single.MinValue
            Next
        Next
    End Sub

    ''' <summary>
    ''' Recalculate min/max values for each variable and/or segment
    ''' </summary>
    Private Sub RecalcMinMax()
        If Not _InRam Then Exit Sub
        ResetMinMax()
        With _Header
            For t As Integer = 0 To .NumTimes - 1
                For s As Integer = 0 To .NumSegments - 1
                    For v As Integer = 0 To _Header.NumVars - 1
                        RecalcMinMax(v, s, _Conc(t, s, v))
                    Next
                Next
            Next
        End With
    End Sub

    ''' <summary>
    ''' Recalculate min/max values for a single new concentration
    ''' </summary>
    Private Sub RecalcMinMax(ByVal VarIndex As Integer, ByVal SegIndex As Integer, ByVal Conc As Single)
        _MinOverVars(VarIndex) = Math.Min(_MinOverVars(VarIndex), Conc)
        _MaxOverVars(VarIndex) = Math.Max(_MaxOverVars(VarIndex), Conc)
        _MinOverVarSegs(VarIndex, SegIndex) = Math.Min(_MinOverVarSegs(VarIndex, SegIndex), Conc)
        _MaxOverVarSegs(VarIndex, SegIndex) = Math.Max(_MaxOverVarSegs(VarIndex, SegIndex), Conc)
    End Sub

    ''' <summary>
    ''' Store concentration for given variable at segment and time
    ''' </summary>
    ''' <param name="VarName">Variable name</param>
    ''' <param name="SegName">Segment name</param>
    ''' <param name="TimeIndex">Time index</param>
    Public Property Conc(ByVal VarName As String, ByVal SegName As String, ByVal TimeIndex As Integer) As Single
        Get
            Return Conc(VarIndex(VarName), SegmentIndex(SegName), TimeIndex)
        End Get
        Set(ByVal Value As Single)
            Conc(VarIndex(VarName), SegmentIndex(SegName), TimeIndex) = Value
        End Set
    End Property

    ''' <summary>
    ''' Get all data for given variable and time (good for profiles)
    ''' </summary>
    ''' <param name="VarName">Variable name</param>
    ''' <param name="DateTime">Date value</param>
    Public ReadOnly Property Conc(ByVal VarName As String, ByVal DateTime As Date) As Single()
        Get
            Return Conc(VarIndex(VarName), DateTime)
        End Get
    End Property

    ''' <summary>
    ''' Get all data for given variable and time (good for profiles)
    ''' </summary>
    ''' <param name="VarIndex">Variable index</param>
    ''' <param name="DateTime">Date value</param>
    Public ReadOnly Property Conc(ByVal VarIndex As Integer, ByVal DateTime As Date) As Single()
        Get
            Dim t As Integer = TimeIndex(DateTime)
            CheckIndices(VarIndex, , t)
            With _Header
                Dim c(.NumSegments - 1) As Single
                If t >= 0 And t <= .NumTimes - 1 Then
                    For s As Integer = 0 To .NumSegments - 1
                        c(s) = Conc(VarIndex, s, t)
                    Next
                End If
                Return c
            End With
        End Get
    End Property

    ''' <summary>
    ''' Check validity of one or more indices and throw exception if necessary
    ''' </summary>
    Private Sub CheckIndices(Optional ByVal VarIndex As Integer = Integer.MinValue, Optional ByVal SegIndex As Integer = Integer.MinValue, Optional ByVal TimeIndex As Integer = Integer.MinValue)
        With _Header
            If (VarIndex <> Integer.MinValue AndAlso (VarIndex < 0 OrElse VarIndex > .NumVars - 1)) OrElse _
               (SegIndex <> Integer.MinValue AndAlso (SegIndex < 0 OrElse SegIndex > .NumSegments - 1)) OrElse _
               (TimeIndex <> Integer.MinValue AndAlso (TimeIndex < 0 OrElse TimeIndex > .NumTimes - 1)) Then
                Debug.Print("Vars: {0}/{1}; Segs: {2}/{3}; Times: {4}/{5}", VarIndex, .NumVars, SegIndex, .NumSegments, TimeIndex, .NumTimes)
                Throw New ApplicationException("Out of bounds exception: invalid indices were used when trying to retrieve concentration.")
            End If
        End With
    End Sub

    ''' <summary>
    ''' Store concentration for given variable at segment and time
    ''' </summary>
    ''' <param name="VarIndex">Variable index</param>
    ''' <param name="SegIndex">Segment index</param>
    ''' <param name="TimeIndex">Time index</param>
    Public Property Conc(ByVal VarIndex As Integer, ByVal SegIndex As Integer, ByVal TimeIndex As Integer) As Single
        Get
            CheckIndices(VarIndex, SegIndex, TimeIndex)
            If _InRam AndAlso ReadConcs() Then
                Return _Conc(TimeIndex, SegIndex, VarIndex)
            Else
                Dim loc As Long = locConc + ((CLng(TimeIndex) * _Header.NumSegments + SegIndex) * _Header.NumVars + VarIndex) * ConcSize
                _File.Seek(loc, IO.SeekOrigin.Begin)
                Return _BR.ReadSingle
            End If
        End Get
        Set(ByVal Value As Single)
            If _InRam AndAlso RedimConcs() Then
                CheckIndices(VarIndex, SegIndex, TimeIndex)
                _Conc(TimeIndex, SegIndex, VarIndex) = Value
            Else
                CheckIndices(VarIndex, SegIndex, )
                _File.Seek(locConc + ((CLng(TimeIndex) * _Header.NumSegments + SegIndex) * _Header.NumVars + VarIndex) * ConcSize, IO.SeekOrigin.Begin)
                _BW.Write(Value)
                RecalcMinMax(VarIndex, SegIndex, Value)
                If TimeIndex + 1 > _Header.NumTimes Then _Header.NumTimes = TimeIndex + 1
            End If
        End Set
    End Property

    ''' <summary>
    ''' Store concentrations for all variables at a given segment and time
    ''' </summary>
    ''' <param name="SegName">Segment name</param>
    ''' <param name="TimeIndex">Time index</param>
    ''' <value>Concentration</value>
    Public Property Conc(ByVal SegName As String, ByVal TimeIndex As Integer) As Single()
        Get
            Return Conc(SegmentIndex(SegName), TimeIndex)
        End Get
        Set(ByVal value As Single())
            Conc(SegmentIndex(SegName), TimeIndex) = value
        End Set
    End Property

    ''' <summary>
    ''' Return concentrations at all times for a given station and variable (good for time series)
    ''' </summary>
    Public ReadOnly Property Conc(ByVal SegName As String, ByVal VarName As String) As Single()
        Get
            With _Header
                Dim s As Integer = SegmentIndex(SegName)
                Dim v As Integer = VarIndex(VarName)
                Dim c(.NumTimes - 1) As Single
                For i As Integer = 0 To .NumTimes - 1
                    c(i) = Conc(v, s, i)
                Next
                Return c
            End With
        End Get
    End Property

    ''' <summary>
    ''' Store concentrations for all variables at a given segment and time
    ''' </summary>
    ''' <param name="SegIndex">Segment index</param>
    ''' <param name="TimeIndex">Time index</param>
    ''' <value>Concentration</value>
    Public Property Conc(ByVal SegIndex As Integer, ByVal TimeIndex As Integer) As Single()
        Get
            With _Header
                CheckIndices(, SegIndex, TimeIndex)
                Dim ar(.NumVars - 1) As Single
                If _InRam AndAlso ReadConcs() Then
                    For i As Integer = 0 To .NumVars - 1
                        ar(i) = _Conc(TimeIndex, SegIndex, i)
                    Next
                    Return ar
                Else
                    _File.Seek(locConc + ((CLng(TimeIndex) * .NumSegments + SegIndex) * .NumVars) * ConcSize, IO.SeekOrigin.Begin)
                    Dim byt() As Byte = Nothing
                    ReDim byt(.NumVars * ConcSize - 1)
                    _BR.Read(byt, 0, byt.Length)
                    Buffer.BlockCopy(byt, 0, ar, 0, byt.Length)
                    'For i As Integer = 0 To .NumVars - 1
                    '    ar(i) = _BR.ReadSingle
                    'Next
                    Return ar
                End If
            End With
        End Get
        Set(ByVal Value() As Single)
            If _InRam AndAlso RedimConcs() Then
                CheckIndices(, SegIndex, TimeIndex)
                For i As Integer = 0 To _Header.NumVars - 1
                    _Conc(TimeIndex, SegIndex, i) = Value(i)
                Next
            Else
                CheckIndices(, SegIndex, )
                _File.Seek(locConc + ((CLng(TimeIndex) * _Header.NumSegments + SegIndex) * _Header.NumVars) * ConcSize, IO.SeekOrigin.Begin)
                Dim ByteArray(_Header.NumVars * ConcSize - 1) As Byte
                Buffer.BlockCopy(Value, 0, ByteArray, 0, _Header.NumVars * ConcSize)
                _BW.Write(ByteArray)

                'update min/max values in case are reading these during write operations
                For v As Integer = 0 To _Header.NumVars - 1
                    RecalcMinMax(v, SegIndex, Value(v))
                Next
                If TimeIndex + 1 > _Header.NumTimes Then _Header.NumTimes = TimeIndex + 1
            End If
        End Set
    End Property

    ''' <summary>
    ''' Store concentrations for all variables and segments at a given time (array indices are [segs,vars]).
    ''' When reading, must do so in sequential time order because seek only done for 0 index
    ''' </summary>
    ''' <param name="TimeIndex">Time index</param>
    ''' <param name="ForceSeek">Set to true if want to seek to intermediate time in file then read all sequentially (will always do seek if first time)</param>
    ''' <value>Array of concentrations at given time [seg,var]</value>
    Public Property Conc(ByVal TimeIndex As Integer, Optional ForceSeek As Boolean = False) As Single(,)
        Get
            CheckIndices(, , TimeIndex)
            Dim ar(_Header.NumSegments - 1, _Header.NumVars - 1) As Single
            If _InRam AndAlso ReadConcs() Then
                For v As Integer = 0 To _Header.NumVars - 1
                    For s As Integer = 0 To _Header.NumSegments - 1
                        ar(s, v) = _Conc(TimeIndex, s, v)
                    Next
                Next
                Return ar
            Else
                If TimeIndex = 0 OrElse ForceSeek Then _File.Seek(locConc + (CLng(TimeIndex) * _Header.NumSegments * _Header.NumVars) * ConcSize, IO.SeekOrigin.Begin)

                'use following procedure to really speed up the read operation
                Dim byt() As Byte = Nothing
                Try
                    ReDim byt(_Header.NumVars * _Header.NumSegments * ConcSize - 1)
                    _BR.Read(byt, 0, byt.Length)
                    'note: data are stored in a different order, so can't read directly into array; rearrange
                    Buffer.BlockCopy(byt, 0, ar, 0, byt.Length)
                Catch ex As OutOfMemoryException
                    Throw New OutOfMemoryException(MemoryError, ex)
                    Return Nothing
                Finally
                    If byt IsNot Nothing Then Erase byt
                End Try
                Return ar
            End If
        End Get
        Set(ByVal Value As Single(,))
            If _InRam AndAlso RedimConcs() Then
                CheckIndices(, , TimeIndex)
                For v As Integer = 0 To _Header.NumVars - 1
                    For s As Integer = 0 To _Header.NumSegments - 1
                        _Conc(TimeIndex, s, v) = Value(s, v)
                    Next
                Next
            Else
                If TimeIndex = 0 OrElse ForceSeek Then _File.Seek(locConc + (CLng(TimeIndex) * _Header.NumSegments * _Header.NumVars) * ConcSize, IO.SeekOrigin.Begin)
                'use following procedure to really speed up the read operation
                Dim byt() As Byte = Nothing
                Try
                    ReDim byt(_Header.NumVars * _Header.NumSegments * ConcSize - 1)
                    Buffer.BlockCopy(Value, 0, byt, 0, byt.Length)
                    _BW.Write(byt, 0, byt.Length)
                Catch ex As OutOfMemoryException
                    Throw New OutOfMemoryException(MemoryError, ex)
                    Exit Property
                Finally
                    If byt IsNot Nothing Then Erase byt
                End Try

                'update min/max values in case are reading these during write operations
                For s As Integer = 0 To _Header.NumSegments - 1
                    For v As Integer = 0 To _Header.NumVars - 1
                        RecalcMinMax(v, s, Value(s, v))
                        If TimeIndex + 1 > _Header.NumTimes Then _Header.NumTimes = TimeIndex + 1
                    Next
                Next
            End If
        End Set
    End Property

    ''' <summary>
    ''' Last date in file
    ''' </summary>
    Public Property EndTime() As Date
        Get
            Return GetDate(_Header.EndTime)
        End Get
        Set(ByVal Value As Date)
            _Header.EndTime = GetDouble(Value)
        End Set
    End Property

    ''' <summary>
    ''' Number of segments (must set before times or concs)
    ''' </summary>
    Public Property NumSegments() As Integer
        Get
            Return _Header.NumSegments
        End Get
        Set(ByVal Value As Integer)
            _Header.NumSegments = Value
            ReDim _SegmentNames(Value - 1)
            If _InRam Then ReDim _Conc(_Header.NumTimes - 1, _Header.NumSegments - 1, _Header.NumVars - 1)
            ComputeLocs()
            ResetMinMax()
        End Set
    End Property

    ''' <summary>
    ''' Number of time steps (must set before times or concs)
    ''' </summary>
    Public Property NumTimes() As Integer
        Get
            Return _Header.NumTimes
        End Get
        Set(ByVal Value As Integer)
            _Header.NumTimes = Value
            ReDim _Times(Value - 1)
            ComputeLocs()
        End Set
    End Property

    ''' <summary>
    ''' Number of variables (must set before times or concs)
    ''' </summary>
    Public Property NumVars() As Integer
        Get
            Return _Header.NumVars
        End Get
        Set(ByVal Value As Integer)
            _Header.NumVars = Value
            ReDim _VarNames(Value - 1)
            ReDim _VarUnits(Value - 1)
            ComputeLocs()
            ResetMinMax()
        End Set
    End Property

    ''' <summary>
    ''' Name of specified segment (truncates to 15 characters)
    ''' </summary>
    ''' <param name="SegIndex">Segment number</param>
    ''' <remarks>Read in ReadHeader, saved in Close</remarks>
    Public Property SegmentName(ByVal SegIndex As Integer) As String
        Get
            Return _SegmentNames(SegIndex)
        End Get
        Set(ByVal Value As String)
            _SegmentNames(SegIndex) = Value
        End Set
    End Property

    ''' <summary>
    ''' List of all segment names
    ''' </summary>
    Public ReadOnly Property SegmentNames() As String()
        Get
            Return _SegmentNames
        End Get
    End Property

    ''' <summary>
    ''' Return indexed of specified segment name
    ''' </summary>
    ''' <param name="SegmentName">Name of segment</param>
    Public Function SegmentIndex(ByVal SegmentName As String) As Integer
        Return Array.IndexOf(_SegmentNames, SegmentName)
    End Function

    ''' <summary>
    ''' Starting time
    ''' </summary>
    Public Property StartTime() As Date
        Get
            Return GetDate(_Header.StartTime)
        End Get
        Set(ByVal Value As Date)
            _Header.StartTime = GetDouble(Value)
        End Set
    End Property

    ''' <summary>
    ''' Variable name (will truncate to 18 characters)
    ''' </summary>
    ''' <param name="VarIndex">Variable index</param>
    Public Property VarName(ByVal VarIndex As Integer) As String
        Get
            Return _VarNames(VarIndex)
        End Get
        Set(ByVal Value As String)
            _VarNames(VarIndex) = Value
        End Set
    End Property

    ''' <summary>
    ''' Variable units (will truncate to 12 characters)
    ''' </summary>
    ''' <param name="VarIndex">Variable index</param>
    Public Property VarUnits(ByVal VarIndex As Integer) As String
        Get
            Return _VarUnits(VarIndex)
        End Get
        Set(ByVal Value As String)
            _VarUnits(VarIndex) = Value
        End Set
    End Property

    ''' <summary>
    ''' Return indexed of specified variable
    ''' </summary>
    ''' <param name="VarName">Name of variable</param>
    Public Function VarIndex(ByVal VarName As String) As Integer
        Return Array.IndexOf(_VarNames, VarName)
    End Function

    ''' <summary>
    ''' Store time as specified index (not actually written to BMD file until it is closed); should be in increasing order
    ''' </summary>
    Public Property Time(ByVal TimeIndex As Integer) As Date
        Get
            If _Times Is Nothing Then ReadTimes()
            If TimeIndex > _Header.NumTimes - 1 Then Return CDate("1/1/1901")
            Return GetDate(_Times(TimeIndex))
        End Get
        Set(ByVal Value As Date)
            If TimeIndex > UBound(_Times) Then ReDim Preserve _Times(UBound(_Times) + 500)
            If TimeIndex + 1 > _Header.NumTimes Then _Header.NumTimes = TimeIndex + 1
            _Times(TimeIndex) = GetDouble(Value)
        End Set
    End Property

    ''' <summary>
    ''' All times (not actually written to BMD file until it is closed); should be in increasing order
    ''' </summary>
    Public Property Times() As Date()
        Get
            If _Times Is Nothing Then ReadTimes()
            With _Header
                Dim t(.NumTimes - 1) As Date
                For i As Integer = 0 To .NumTimes - 1
                    t(i) = GetDate(_Times(i))
                Next
                Return t
            End With
        End Get
        Set(ByVal value As Date())
            With _Header
                ReDim _Times(value.Length - 1)
                For i As Integer = 0 To .NumTimes - 1
                    _Times(i) = GetDouble(value(i))
                Next
                .NumTimes = value.Length
                ComputeLocs()
            End With
        End Set
    End Property

    ''' <summary>
    ''' Return closest index of specified time
    ''' </summary>
    ''' <param name="DateTime">Desired date</param>
    Public Function TimeIndex(ByVal DateTime As Date) As Integer
        If _Times Is Nothing Then ReadTimes()
        Dim dt As Double = GetDouble(DateTime)
        With _Header
            If dt < _Times(0) Then Return 0
            If dt > _Times(.NumTimes - 1) Then Return .NumTimes - 1
            'If dt < _Times(0) - 0.0001 Or dt > _Times(.NumTimes - 1) + 0.0001 Then Return -1 'flag that is out of bounds
            Dim RecNum As Integer = Array.BinarySearch(_Times, 0, .NumTimes, dt)
            If RecNum < 0 Then RecNum = RecNum Xor -1 'not found, returns bitwise element of next larger value
            If RecNum > .NumTimes - 1 Then Return .NumTimes - 1 'round-off may make it slightly higher and cause index error
            If RecNum > 0 Then 'see if prior one in array is actually closer in value
                If Math.Abs(_Times(RecNum - 1) - dt) < Math.Abs(_Times(RecNum) - dt) Then RecNum -= 1
            End If
            Return RecNum
            Return Array.BinarySearch(Times, DateTime.AddSeconds(-1))
        End With
    End Function

    ''' <summary>
    ''' Minimum value for a given variable for all segments and times
    ''' </summary>
    ''' <param name="VarName">Variable name</param>
    Public Property VarMin(ByVal VarName As String) As Single
        Get
            Return VarMin(VarIndex(VarName))
        End Get
        Set(ByVal value As Single)
            VarMin(VarIndex(VarName)) = value
        End Set
    End Property

    ''' <summary>
    ''' Maximum value for a given variable for all segments and times
    ''' </summary>
    ''' <param name="VarName">Variable name</param>
    Public Property VarMax(ByVal VarName As String) As Single
        Get
            Return VarMax(VarIndex(VarName))
        End Get
        Set(ByVal value As Single)
            VarMax(VarIndex(VarName)) = value
        End Set
    End Property

    ''' <summary>
    ''' Minimum value for a given variable and segment for all times
    ''' </summary>
    ''' <param name="VarName">Variable name</param>
    ''' <param name="SegName">Segment name</param>
    Public Property VarSegMin(ByVal VarName As String, ByVal SegName As String) As Single
        Get
            Return VarSegMin(VarIndex(VarName), SegmentIndex(SegName))
        End Get
        Set(ByVal value As Single)
            VarSegMin(VarIndex(VarName), SegmentIndex(SegName)) = value
        End Set
    End Property

    ''' <summary>
    ''' Maximum value for a given variable and segment for all times
    ''' </summary>
    ''' <param name="VarName">Variable name</param>
    ''' <param name="SegName">Segment name</param>
    Public Property VarSegMax(ByVal VarName As String, ByVal SegName As String) As Single
        Get
            Return VarSegMax(VarIndex(VarName), SegmentIndex(SegName))
        End Get
        Set(ByVal value As Single)
            VarSegMax(VarIndex(VarName), SegmentIndex(SegName)) = value
        End Set
    End Property

    ''' <summary>
    ''' Maximum value for a given variable for all segments and times
    ''' </summary>
    ''' <param name="VarIndex">Variable index</param>
    Public Property VarMax(ByVal VarIndex As Integer) As Single
        Get
            Return _MaxOverVars(VarIndex)
        End Get
        Set(ByVal Value As Single)
            _MaxOverVars(VarIndex) = Value
        End Set
    End Property

    ''' <summary>
    ''' Minimum value for a given variable for all segments and times
    ''' </summary>
    ''' <param name="VarIndex">Variable index</param>
    Public Property VarMin(ByVal VarIndex As Integer) As Single
        Get
            Return _MinOverVars(VarIndex)
        End Get
        Set(ByVal Value As Single)
            _MinOverVars(VarIndex) = Value
        End Set
    End Property

    ''' <summary>
    ''' Maximum value for a given variable and segment for all times
    ''' </summary>
    ''' <param name="VarIndex">Variable index</param>
    ''' <param name="SegIndex">Segment index</param>
    Public Property VarSegMax(ByVal VarIndex As Integer, ByVal SegIndex As Integer) As Single
        Get
            Return _MaxOverVarSegs(VarIndex, SegIndex)
        End Get
        Set(ByVal Value As Single)
            _MaxOverVarSegs(VarIndex, SegIndex) = Value
        End Set
    End Property

    ''' <summary>
    ''' Minimum value for a given variable and segment for all times
    ''' </summary>
    ''' <param name="VarIndex">Variable index</param>
    ''' <param name="SegIndex">Segment index</param>
    Public Property VarSegMin(ByVal VarIndex As Integer, ByVal SegIndex As Integer) As Single
        Get
            Return _MinOverVarSegs(VarIndex, SegIndex)
        End Get
        Set(ByVal Value As Single)
            _MinOverVarSegs(VarIndex, SegIndex) = Value
        End Set
    End Property

    Public Property InRam() As Boolean
        Get
            Return _InRam
        End Get
        Set(ByVal value As Boolean)
            _InRam = value
        End Set
    End Property

    ''' <summary>
    ''' Round date-time value to the nearest minute
    ''' </summary>
    ''' <param name="Original">Value to be rounded</param>
    ''' <returns></returns>
    ''' <remarks>This is necessary because slight round-off errors when truncated will appear as, e.g., 11:59 rather than 12:00</remarks>
    Private Function RoundDateTime(ByVal Original As Date) As Date
        With Original
            If .Second >= 30 Then Original = .AddMinutes(1)
            Return New Date(.Year, .Month, .Day, .Hour, .Minute, 0)
        End With
    End Function
End Class